package git.soulbgm.job;

import java.io.IOException;
import java.util.Date;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import git.soulbgm.common.enums.Status;
import git.soulbgm.common.okhttp.OkHttpUtil;
import git.soulbgm.config.SysConfig;
import git.soulbgm.pojo.*;
import git.soulbgm.pojo.dto.AliveInfo;
import git.soulbgm.service.*;
import git.soulbgm.utils.JsonTool;
import git.soulbgm.utils.LogHelper;
import git.soulbgm.utils.StringUtil;
import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.Response;
import okhttp3.ResponseBody;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;

import java.util.*;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.Collectors;

import static git.soulbgm.common.MonitorConstants.*;

/**
 * 软件健康监测任务
 *
 * @author SoulBGM
 * @date 2021/01/29
 */
@Service
public class SoftwareHealthMonitorTask {

    private static final LogHelper LOG = new LogHelper(SoftwareHealthMonitorTask.class);
    private static final String SEPARATOR = ":";

    private final SoftwareInfoService softwareInfoService;
    private final SoftwareStatusService softwareStatusService;
    private final NodeInfoService nodeInfoService;
    private final ServiceStartingService serviceStartingService;
    private final StartupService startupService;
    private final AlarmInfoReport alarmInfoReport;
    private final Integer currentRegionCode;

    public SoftwareHealthMonitorTask(SoftwareInfoService softwareInfoService, SoftwareStatusService softwareStatusService, NodeInfoService nodeInfoService, ServiceStartingService serviceStartingService, StartupService startupService, AlarmInfoReport alarmInfoReport, SysConfig config) {
        this.softwareInfoService = softwareInfoService;
        this.softwareStatusService = softwareStatusService;
        this.nodeInfoService = nodeInfoService;
        this.serviceStartingService = serviceStartingService;
        this.startupService = startupService;
        this.alarmInfoReport = alarmInfoReport;
        this.currentRegionCode = config.getRegionCode();
    }

    @Scheduled(cron = "${jobs.map.softwareHealthMonitoring.cron}")
    @SchedulerLock(name = "${jobs.map.softwareHealthMonitoring.name}", lockAtMostFor = "${jobs.map.softwareHealthMonitoring.lockAtMostFor}", lockAtLeastFor = "${jobs.map.softwareHealthMonitoring.lockAtLeastFor}")
    public void execute() throws Exception {
        Date collectTime = new Date();
        Map<String, NodeInfo> nodeInfoMap = nodeInfoService.getCacheNodeInfoAll().stream().collect(Collectors.toMap(o -> o.getIp() + SEPARATOR + o.getNodeCode(), Function.identity()));
        List<SoftwareInfo> softwareInfoList = softwareInfoService.getCacheSoftwareInfoAll().stream().filter(o -> Status.NORMAL.code.equals(o.getStatus()) && MONITOR_MODE_PASSIVITY == o.getMonitorMode()).collect(Collectors.toList());
        if (softwareInfoList.isEmpty()) {
            return;
        }
        Map<Long, SoftwareInfo> softwareInfoMap = softwareInfoList.stream().collect(Collectors.toMap(SoftwareInfo::getId, Function.identity()));
        Map<String, List<SoftwareInfo>> map = softwareInfoList.stream().collect(Collectors.groupingBy(o -> o.getDeployIp() + SEPARATOR + o.getNodeCode()));
        int size = map.size();
        Set<String> keySet = map.keySet();
        Map<String, List<SoftwareStatus>> statusMap = new LinkedHashMap<>(size);
        keySet.forEach(k -> statusMap.put(k, getSoftwareStatusList(map.get(k), collectTime)));
        CountDownLatch latch = new CountDownLatch(size);
        for (String k : keySet) {
            NodeInfo info = nodeInfoMap.get(k);
            List<SoftwareStatus> softwareStatusList = statusMap.get(k);
            String url = StringUtil.format("http://{}:{}{}", info.getIp(), info.getPort(), IS_ALIVE_URL);
            OkHttpUtil.get(url, setParams(map.get(k)), new SoftwareHealthMonitorCallback(softwareStatusList, latch));
        }

        boolean isExecuteComplete = latch.await(10L * size, TimeUnit.SECONDS);
        if (!isExecuteComplete) {
            LOG.error("执行软件健康监测时间超时");
        }

        List<SoftwareStatus> statusList = new ArrayList<>(softwareInfoList.size());
        statusMap.values().forEach(statusList::addAll);
        statusList = statusList.stream().sorted(Comparator.comparing(SoftwareStatus::getId)).collect(Collectors.toList());

        for (SoftwareStatus softwareStatus : statusList) {
            SoftwareInfo info = softwareInfoMap.get(softwareStatus.getId());
            softwareStatus.setStatus(checkStartup(softwareStatus, info));
            recordAlarm(softwareStatus);
        }

        softwareStatusService.reset(statusList);
    }

    /**
     * 获得软件状态列表
     *
     * @param infos       信息列表
     * @param collectTime 收集时间
     * @return 参数
     */
    private List<SoftwareStatus> getSoftwareStatusList(List<SoftwareInfo> infos, Date collectTime) {
        List<SoftwareStatus> softwareStatusList = new ArrayList<>(infos.size());
        for (SoftwareInfo info : infos) {
            SoftwareStatus softwareStatus = new SoftwareStatus();
            softwareStatus.setId(info.getId());
            softwareStatus.setSoftwareName(info.getSoftwareName());
            softwareStatus.setProcessName(info.getProcessName());
            softwareStatus.setNodeCode(info.getNodeCode());
            softwareStatus.setDeployIp(info.getDeployIp());
            softwareStatus.setCheckPort(info.getCheckPort());
            softwareStatus.setSoftwareStartupTime(0L);
            softwareStatus.setDetectionTime(collectTime);
            softwareStatus.setRegionCode(currentRegionCode);
            softwareStatus.setStatus(Status.NOT_INITIATED.code);
            softwareStatusList.add(softwareStatus);
        }
        return softwareStatusList;
    }

    /**
     * 设置参数
     *
     * @param infoList 软件信息列表
     * @return {@link Map}<{@link String}, {@link Object}>
     */
    private Map<String, Object> setParams(List<SoftwareInfo> infoList) {
        StringBuilder processNames = new StringBuilder();
        for (SoftwareInfo s : infoList) {
            processNames.append(s.getProcessName());
            Integer checkPort = s.getCheckPort();
            if (checkPort != 0) {
                processNames.append(SEPARATOR).append(checkPort);
            }
            processNames.append(",");
        }
        if (infoList.size() > 0) {
            processNames.deleteCharAt(processNames.length() - 1);
        }
        return Collections.singletonMap("processNames", processNames.toString());
    }

    /**
     * 检查启动
     *
     * @param softwareStatus 软件状态
     * @param info           软件信息
     * @return {@link Integer}
     */
    private Integer checkStartup(SoftwareStatus softwareStatus, SoftwareInfo info) {
        Integer status = softwareStatus.getStatus();
        Integer selfStart = info.getSelfStart();
        Integer estimatedStartupTime = info.getEstimatedStartupTime();

        ServiceStarting starting = new ServiceStarting(softwareStatus.getNodeCode(),
                softwareStatus.getSoftwareName() + "_" + softwareStatus.getProcessName(),
                RULE_TYPE_SOFTWARE,
                softwareStatus.getDeployIp());
        if (Status.NOT_INITIATED.code.equals(status)) {
            // 查看 启动表中是否有记录
            ServiceStarting serviceStarting = serviceStartingService.getOne(new QueryWrapper<>(starting));
            // 如果没有则添加
            if (serviceStarting == null && Status.YES.code.equals(selfStart)) {
                starting.setStartupTime(new Date());
                serviceStartingService.save(starting);
                startupService.startupSoftware(softwareStatus.getNodeCode(), softwareStatus.getSoftwareName(), softwareStatus.getProcessName());
                status = Status.STARTING.code;
            } else if (serviceStarting != null) {
                // 如果有则判断是否超过预计时间
                Date startUpTime = serviceStarting.getStartupTime();
                Date nowDate = new Date();
                long difference = nowDate.getTime() - startUpTime.getTime();
                if ((difference / ONE_SECONDS_MILLISECOND) >= estimatedStartupTime) {
                    // 如果超过则设置状态为 未启动
                    status = Status.NOT_INITIATED.code;
                } else {
                    // 如果没有超过则设置状态为 启动中
                    status = Status.STARTING.code;
                }
            } else {
                // 不需要自动启动则设置状态为 未启动
                status = Status.NOT_INITIATED.code;
            }
        } else if (Status.NORMAL.code.equals(status)) {
            // 删除 启动表中的记录
            serviceStartingService.remove(new QueryWrapper<>(starting));
        }
        return status;
    }

    /**
     * 记录报警
     *
     * @param softwareStatus 软件状态
     */
    public void recordAlarm(SoftwareStatus softwareStatus) {
        Integer status = softwareStatus.getStatus();
        if (Status.STARTING.code.equals(status)) {
            // 软件重启
            String alarmContent = StringUtil.format("{} 中间件的 {} 进程在IP为 {}的服务器节点宕机，现在正在尝试重启", softwareStatus.getSoftwareName(), softwareStatus.getProcessName(), softwareStatus.getDeployIp());
            LOG.warn(alarmContent);
            alarmInfoReport.softwareRebootAlarm(softwareStatus.getNodeCode(), softwareStatus.getProcessName(), softwareStatus.getCheckPort(), alarmContent);
        } else if (Status.NOT_INITIATED.code.equals(status)) {
            // 软件宕机
            String alarmContent = StringUtil.format("{} 中间件的 {} 进程在IP为 {}的服务器节点，尝试重启没有成功现在处于宕机", softwareStatus.getSoftwareName(), softwareStatus.getProcessName(), softwareStatus.getDeployIp());
            LOG.warn(alarmContent);
            alarmInfoReport.softwareShutdownAlarm(softwareStatus.getNodeCode(), softwareStatus.getProcessName(), softwareStatus.getCheckPort(), alarmContent);
        } else if (Status.NORMAL.code.equals(status)) {
            // 软件正常
            alarmInfoReport.softwareStartingClearAlarm(softwareStatus.getNodeCode(), softwareStatus.getProcessName(), softwareStatus.getCheckPort());
        }
    }

}

/**
 * 软件健康监测回调
 *
 * @author SoulBGM
 * @date 2023-01-06
 */
class SoftwareHealthMonitorCallback implements Callback {

    private static final String SEPARATOR = ":";

    private final List<SoftwareStatus> softwareStatusList;
    private final CountDownLatch latch;

    public SoftwareHealthMonitorCallback(List<SoftwareStatus> softwareStatusList, CountDownLatch latch) {
        this.softwareStatusList = softwareStatusList;
        this.latch = latch;
    }

    @Override
    public void onResponse(Call call, Response response) throws IOException {
        ResponseBody body = response.body();
        if (body != null) {
            List<AliveInfo> aliveInfos = JsonTool.parseArray(body.string(), AliveInfo.class);
            if (aliveInfos != null) {
                Map<String, AliveInfo> statusMap = aliveInfos.stream().collect(Collectors.toMap(AliveInfo::getName, Function.identity()));
                for (SoftwareStatus s : softwareStatusList) {
                    String processName = s.getProcessName();
                    String key = processName + SEPARATOR + s.getCheckPort();
                    AliveInfo info = statusMap.getOrDefault(key, statusMap.getOrDefault(processName, AliveInfo.down(processName)));
                    Boolean isAlive = info.getStatus();
                    s.setSoftwareStartupTime(info.getStartupTime());
                    s.setStatus(isAlive ? Status.NORMAL.code : Status.NOT_INITIATED.code);
                }
            }
        }
        latch.countDown();
    }

    @Override
    public void onFailure(Call call, IOException e) {
        latch.countDown();
    }
}